package org.xtlgen;

import java.util.ArrayList;
import java.util.TreeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;

/**
 * Abstract base class for all the Shapes in the XtlGen system.
 *
 * <p>
 * A subclass must provide implementations for {@link #createAWTShape},
 * {@link #clipAWTShape} and might also need to override {@link #getChildren},
 * {@link #drawSelf} and {@link #drawChildren}.
 * </p>
 * <p>
 * Subclasses may also register a {@link ShapeFactory} so its instances
 * can be created through the main Shape factory method
 * {@link #makeShape}.
 * A convenient way to do this is to use an static block of code, like this:
 * </p>
 * <pre>
 *   static {
 *     Shape.registerShapeFactory(
 *       "Rectangle",
 *       new Shape.ShapeFactory() {
 *         public Shape makeShape(Shape parent, Element params) {
 *           return new Rectangle(parent, params);
 *         }
 *       });
 *   }
 * </pre>
 * <p>
 * The Shape factory ensures the block has been executed before attempting
 * to find a factory by obtaining a reference to the Shape's Class object.
 * </p>
 *
 */
public abstract class Shape {
  /**
   * Construct a Shape from the given AWT Shape
   * Delegates clipping to subclasses.
   *
   * @param parent   the parent Shape, null allowed
   * @param awtShape the AWT Shape
   */
  protected Shape(Shape parent, java.awt.Shape awtShape) {
    this.parent = parent;
    this.children = new ArrayList<Shape>();
    this.actualShape = awtShape;
    if (parent == null) {
      this.clippedShape = this.actualShape;
    }
    else {
      this.clippedShape = clipAWTShape(this.parent, this.actualShape);
    }
  }
  
  /**
   * Construct a Shape.
   * Delegates AWT shape creation, clipping and children creation to subclasses.
   *
   * @param parent  the parent Shape, null allowed
   * @param params  parameters for this Shape as read from the config file
   */
  protected Shape(Shape parent, Element params) {
    this.parent = parent;
    this.children = new ArrayList<Shape>();
    this.actualShape = createAWTShape(params);
    if (parent == null) {
      this.clippedShape = this.actualShape;
    }
    else {
      this.clippedShape = clipAWTShape(this.parent, this.actualShape);
    }
    addChildren(getChildren(params));
  }
  
  /**
   * Display this Shape and children Shapes on the given canvas object.
   * Delegates the drawing to drawSelf and drawChildren.
   *
   * @param canvas  the target canvas
   *
   * @see #drawSelf
   * @see #drawChildren
   */
  public void draw(Canvas canvas) {
    // Draw myself and children if visible
    if (this.clippedShape != null) {
      drawSelf(canvas);
      drawChildren(canvas);
    }
  }

  /**
   * Get a reference to this Shape's parent.
   *
   * @return The parent shape.
   */
  public Shape getParent() {
    return this.parent;
  }

  /**
   * Add a child to this Shape.
   *
   * @param child The new child.
   */
  public void addChild(Shape child) {
    children.add(child);
  }

  /**
   * Get a reference to the clipped AWT shape.
   *
   * @return The clipped AWT shape.
   */
  public java.awt.Shape getAWTShape() {
    return this.clippedShape;
  }

  /**
   * Draw this Shape on the given canvas object.
   * The default implementation calls canvas.draw(this), subclasses may
   * override the method to make this shape invisible or call other
   * operations on the canvas object (e.g. this shape is used as a
   * container only and needs to be invisible).
   *
   * @param canvas  the target canvas
   *
   * @see #draw
   */
  protected void drawSelf(Canvas canvas) {
    canvas.draw(this);
  }

  /**
   * Draw this Shape's children Shapes on the given canvas object.
   * The default implementation calls draw(canvas) for each child, subclasses
   * may override the method if it is known that they do not contain children
   * (e.g. a point or line may not contain subshapes).
   *
   * @param canvas  the target canvas
   *
   * @see #draw
   */
  protected void drawChildren(Canvas canvas) {
      for (Shape child : children) {
        child.draw(canvas);
      }
  }

  /**
   * Construct the AWT shape.
   * Called by the constructor to initialize the AWT shape.
   *
   * @param params parameters from the config file.
   *
   * @return The resulting AWT shape.
   */
  protected abstract java.awt.Shape createAWTShape(Element params);

  /**
   * Clip the AWT shape to the given parent bounds.
   * Called by the constructor to clip the AWT shape.
   *
   * @param parent parent Shape
   * @param shape AWT shape to clip
   *
   * @return The resulting clipped AWT shape or null if completely clipped.
   */
  protected abstract java.awt.Shape clipAWTShape(Shape parent, java.awt.Shape shape);

  /**
   * Get the list of child nodes in params.
   * Called by the constructor to initialize the list of children. May return
   * null if the Shape has no children. The default implementation returns
   * null.
   *
   * @param params parameters from the config file.
   *
   * @return The child node list.
   */
  protected NodeList getChildren(Element params) {
    return null;
  }
  
  /**
   * Add Shapes created from the element nodes in the given node list
   * as children of this Shape.
   *
   * @param children list of children.
   */
  private void addChildren(NodeList children) {
    if (children != null)
    {
      for (int i=0; i<children.getLength(); i++)
      {
        Shape child = Shape.makeShape(this, children.item(i));
        if (child != null)
        {
          this.children.add(child);
        }
      }
    }
  }

  /**
   * Main shape factory, subclasses register individual ShapeFactories
   * by calling registerShapeFactory from static initialization blocks
   *
   * @param parent parent Shape of the Shape to be constructed.
   * @param params parameters from the config file.
   *
   * @return The new Shape
   *
   * @throws IllegalArgumentException If the Node specifies an unknown shape or
   *                                  if a ShapeFactory cannot be found for the Shape type
   *
   * @see #registerShapeFactory
   */
  public static Shape makeShape(Shape parent, Node params) {
    Shape result = null;
    
    if (params.getNodeType() == Node.ELEMENT_NODE)
    {
      try {
        // Force the loading of the class this ensures the subclass factory is registered
        Class shapeClass = Class.forName("org.xtlgen." + params.getNodeName());
      }
      catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Unknown shape found in description file: \"" +
                                           params.getNodeName() + "\"");
      } 
      
      ShapeFactory factory = shapeFactories.get(params.getNodeName());

      if (factory != null)
      {
        result = factory.makeShape(parent, (Element)params);
      }
      else
      {
        throw new IllegalArgumentException("Factory missing for shape class " +
                                           params.getNodeName());
      }
    }
    return result;
  }

  /**
   * Register a ShapeFactory
   *
   * @param shapeName class name of the Shape to be constructed by the given factory
   * @param factory the ShapeFactory
   *
   * @see #makeShape(Shape parent, Node params)
   */
  public static final void registerShapeFactory(String shapeName, ShapeFactory factory) {
    shapeFactories.put(shapeName, factory);
  }

  /**
   * Nested interface implemented by all shape factories
   */
  
  public interface ShapeFactory {
    /**
     * @param parent parent Shape of the Shape to be constructed.
     * @param params parameters from the config file.
     *
     * @return The new Shape
     */
    public Shape makeShape(Shape parent, Element params);
  }
  
  /*
   * Fields
   */
  private Shape   parent;
  private ArrayList<Shape> children;
  private java.awt.Shape   actualShape;
  private java.awt.Shape   clippedShape;

  private static final TreeMap<String, ShapeFactory> shapeFactories =
    new TreeMap<String, ShapeFactory>();
}